package org.metasketch.model.loader.transform;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.StringTokenizer;

import org.metasketch.demo.model.CoordinationLink;
import org.metasketch.demo.model.CoordinationStep;
import org.metasketch.demo.model.CoordinationStepKind;
import org.metasketch.demo.project.load.TransfomPackage;
import org.metasketch.logger.Log;
import org.metasketch.model.CommonIdentified;
import org.metasketch.model.ModelPackage;
import org.metasketch.model.loader.GraphNode;
import org.metasketch.model.loader.IGraphDatabase;
import org.metasketch.reflection.RefUtil;
import org.metasketch.reflection.ReflectionAnalyser;

public class ModelTransformer {

	static RefUtil util = new RefUtil();

	public static ModelPackage transform(IGraphDatabase modelGraph,
			ModelPackage model, String rootTag, String packagePath,
			boolean useTypeAsConstructor) throws Throwable {

		Collection<GraphNode> nodes = modelGraph.getNodes();
		if (nodes.size() > 1) {
			Log.printError("A model can't have more than one " + rootTag
					+ " !!!");
			return null;
		}

		if (nodes.size() == 0) {
			Log.printError("No initial " + rootTag + " node was found !!!");
			return null;
		}

		GraphNode root = nodes.iterator().next();
		if (!root.getTag().equals(rootTag)) {
			Log.printError("The root node is not a " + rootTag + " !!!");
			return null;
		}

		// DEMOModel model = new DEMOModel();

		TransfomPackage transfomPackage = constructModel(root, model,
				packagePath, useTypeAsConstructor);

		assignElements(transfomPackage, model);

		return model;

	}

	private static TransfomPackage constructModel(GraphNode root,
			ModelPackage model, String packagePath, boolean useTypeAsConstructor)
			throws Throwable {

		TransfomPackage transfomPackage = new TransfomPackage();
		// DEMOModel model = new DEMOModel();
		// model.setId(root.getId());
		setupBasicValues(root, model);

		recursiveConstructModel(null, root, model, transfomPackage,
				packagePath, useTypeAsConstructor);

		return transfomPackage;

	}

	private static void recursiveConstructModel(Object rootObject,
			GraphNode root, ModelPackage model,
			TransfomPackage transfomPackage, String packagePath,
			boolean useTypeAsConstructor) throws Throwable {

		List<GraphNode> children = root.getChildren();

		for (GraphNode node : children) {

			Object child = constructElement(node, packagePath,
					useTypeAsConstructor);

			if (child instanceof CommonIdentified) {
				model.addChild((CommonIdentified) child);
				transfomPackage.addElement((CommonIdentified) child, node);
			} else {
				setupChildToParentCollection(rootObject, child, node.getTag());
			}

			recursiveConstructModel(child, node, model, transfomPackage,
					packagePath, useTypeAsConstructor);

		}
	}

	private static Object constructElement(GraphNode root, String packagePath,
			boolean useTypeAsConstructor) throws Throwable {

		String simpleName = null;

		String type = root.getType();
		String tag = root.getTag();

		if (useTypeAsConstructor) {
			simpleName = type;
			if (simpleName == null) {
				simpleName = tag;
				Log.printlnError("Type not found for graphNode with id: "
						+ root.getId() + ". Using tag '" + tag
						+ "' as type name.");
			}

		} else {
			simpleName = tag;
		}

		String path = packagePath + simpleName;

		Class typeClass = null;

		Object object = null;

		if (CoordinationStepKind.getType(simpleName) != null) {

			CoordinationStep coordination = new CoordinationStep();
			coordination.setKind(CoordinationStepKind.getType(simpleName));

			object = coordination;
			typeClass = CoordinationStep.class;

		} else if (simpleName.equals("CausalLink")) {
			CoordinationLink link = new CoordinationLink();
			link.setConditional(false);

			object = link;
			typeClass = CoordinationLink.class;

		} else if (simpleName.equals("ConditionalLink")) {
			CoordinationLink link = new CoordinationLink();
			link.setConditional(true);

			object = link;
			typeClass = CoordinationLink.class;

		} else {

			typeClass = Class.forName(path);

			object = typeClass.newInstance();

		}

		setupBasicValues(root, object);

		// Log.printlnError("Unsupported Node Type: " + type);

		return object;

	}

	private static void setupBasicValues(GraphNode root, Object object)
			throws Exception {
		List<String> basic = ReflectionAnalyser.getBasicFieldNames(object
				.getClass());

		// List<String> basic = report.getBasicFields();
		for (String basicName : basic) {
			String value = root.getProperties().get(basicName);
			if (value != null)
				util.setValue(object, basicName, value);
		}
	}

	private static void assignElements(TransfomPackage tp, ModelPackage model)
			throws Throwable {
		List<String> processedObjects = new ArrayList<String>();

		List<CommonIdentified> children = model.getChildren();
		for (CommonIdentified child : children) {
			String id = child.getId();
			GraphNode node = tp.getElementNodes().get(child);

			recursiveAssignElements(tp, id, child, node, processedObjects);

		}

	}

	private static void recursiveAssignElements(TransfomPackage tp, String id,
			CommonIdentified parent, GraphNode node,
			List<String> processedObjects) throws Exception {

		if (id != null) {
			if (processedObjects.contains(id))
				return;

			processedObjects.add(id);
		}

		List<String> objectFieldNames = ReflectionAnalyser
				.getObjectFieldNames(parent.getClass());
		for (String fieldName : objectFieldNames) {
			String objectId = node.getProperties().get(fieldName);
			if (objectId == null)
				continue;
			CommonIdentified object = tp.getElements().get(objectId);
			// if(object == null)
			util.setValue(parent, fieldName, object);
		}

		List<String> listFieldNames = ReflectionAnalyser
				.getCollectionFieldNames(parent.getClass());
		for (String fieldName : listFieldNames) {
			String objectIds = node.getProperties().get(fieldName);

			if (objectIds == null)
				continue;

			StringTokenizer tk = new StringTokenizer(objectIds);
			while (tk.hasMoreTokens()) {
				String objectId = tk.nextToken();
				CommonIdentified object = tp.getElements().get(objectId);
				util.setValue(parent, fieldName, object);
			}
		}

		List<GraphNode> nodeChildren = node.getChildren();
		for (GraphNode graphChildNode : nodeChildren) {

			String tag = graphChildNode.getTag();
			String type = graphChildNode.getType();

			String cid = graphChildNode.getId();

			CommonIdentified child = tp.getElements().get(cid);

			if (child != null) {
				setupChildToParentCollection(parent, child, tag);

				recursiveAssignElements(tp, cid, child, graphChildNode,
						processedObjects);
			}
		}
	}

	private static void setupChildToParentCollection(Object parent,
			Object child, String tag) throws Exception {

		Class parentClass = parent.getClass();
		Class childClass = child.getClass();

		List<String> compatibleCollections = ReflectionAnalyser
				.getCompatibleCollections(parentClass, childClass);

		if (compatibleCollections.size() == 0) {

			throw new Exception("No collections compatible with child type "
					+ child.getClass().getCanonicalName() + " for parent type "
					+ parent.getClass().getCanonicalName() + " were found.");

		} else if (compatibleCollections.size() == 1) {

			String collectionName = compatibleCollections.get(0);

			if (tag.equals(collectionName)) {
				util.setValue(parent, tag, child);
			} else {
				Log.printlnError("WARNING: The only available collection of child type "
						+ child.getClass().getCanonicalName()
						+ " for parent class "
						+ parent.getClass().getCanonicalName()
						+ " is called '"
						+ collectionName
						+ "' which does not match the tag name '"
						+ tag
						+ "' used in the file. Check metamodel and classes.");
				Log.printlnError("WARNING: Setting up collection '"
						+ collectionName
						+ "' through child element type. This may lead to errors. Check metamodel and classes.");
				ReflectionAnalyser.setObjectToCollectionsPerProperType(parent,
						child);
			}

		} else {
			// if more than one collection were found for the same type,
			// there HAS to be a tag. Otherwise, there is no way to, in good
			// faith, determine which collection is supposed to be used,
			// since there are several different available, with different
			// semantic meanings, which are not clear from this simples
			// data.

			if (!compatibleCollections.contains(tag)) {

				String colls = "";
				for (String collection : compatibleCollections) {
					colls += collection + " ";
				}

				String errorMsg = "Tag '"
						+ tag
						+ "' was not found as the name for several collections ( "
						+ colls + ")  compatible with child type "
						+ child.getClass().getCanonicalName() + " for parent "
						+ parent.getClass().getCanonicalName()
						+ ". Cannot proceed. Check metamodel and classes.";
				Log.printlnError(errorMsg);

				throw new Exception(errorMsg);
			} else
				util.setValue(parent, tag, child);

		}
	}
}
